using System;
using System.Text.RegularExpressions;

namespace Com.SimonProctor.Html
{
	/// <summary>
	/// HTML parser class. Nibbles away at HTML text and fires events
	/// for what is discovered. As the text is nibbled it is consumed
	/// so the original text is not available.
	/// </summary>
	public class Parser
	{
		#region regex

		private System.Text.RegularExpressions.Regex plaintext;
	
		// private System.Text.RegularExpressions.Regex plainstarttag;
		
		private System.Text.RegularExpressions.Regex starttag;
		
		private System.Text.RegularExpressions.Regex closetag;
		
		/// <summary>
		/// For tags that come in pairs.
		/// </summary>
		private System.Text.RegularExpressions.Regex endtag;

		/// <summary>
		/// Handles tags like &lt;img .... /&gt; where the end is in the beginning
		/// </summary>
		private System.Text.RegularExpressions.Regex endsingletag;
	
		private System.Text.RegularExpressions.Regex tagname;
		
		private System.Text.RegularExpressions.Regex endtagname;
	
		private System.Text.RegularExpressions.Regex commentstart;

		// Pattern comment_working = Pattern.compile("^(([^>]*?)--)", Pattern.MULTILINE);
		// private System.Text.RegularExpressions.Regex comment_working;

		private System.Text.RegularExpressions.Regex comment_finish;
	
		// For the attributes
		private System.Text.RegularExpressions.Regex attribute;

		private System.Text.RegularExpressions.Regex unquotedvalue;

		private System.Text.RegularExpressions.Regex quotedvalue;

		#endregion

		#region events
		
		/// <summary>
		/// Start tag event. Fired when a start tag is encountered. The attributes
		/// of this start tag are provided in a hashtable
		/// </summary>
		//public event StartTagEventHandler StartTag;
        public event EventHandler<StartTagEventArgs> StartTag;

		/// <summary>
		/// End tag event. Fired when a tag ends. This will also fire when we 
		/// hit single tags like the img tag.
		/// </summary>
		// public event EndTagEventHandler EndTag;
        public event EventHandler<EndTagEventArgs> EndTag;

		/// <summary>
		/// Text event. Any text that is encountered is fired off. This may be
		/// content or it may be simple white space
		/// </summary>
		// public event TextEventHandler Text;
        public event EventHandler<TextEventArgs> Text;

		/// <summary>
		/// Comment event. The entire comment is consumed and its text is provided.
		/// Currently only supports simple single or multiline comments with no
		/// fancy formatting.
		/// </summary>
		// public event CommentEventHandler Comment;
        public event EventHandler<CommentEventArgs> Comment;

		/// <summary>
		/// Declaration event. Should normally only be fired once when a declaration
		/// is encountered.
		/// </summary>
		// public event DeclarationEventHandler Declaration;
        public event EventHandler<DeclarationEventArgs> Declaration;

		#endregion

		/// <summary>
		/// Generic constructor. Initialises the internal expressions
		/// only.
		/// </summary>
		public Parser()
		{
			initRegex();
		}

		/// <summary>
		/// Initialise the regular expression patterns.
		/// </summary>
		private void initRegex()
		{
			plaintext = new Regex("^([^<]+)");
			// plainstarttag = new Regex("^(<([a-zA-Z]+)>)");
			starttag = new Regex("^<");
			endtag = new Regex("^</");

			closetag = new Regex("^>");
			endsingletag = new Regex("^/?>");

			tagname =  new Regex("^(([a-zA-Z][a-zA-Z0-9\\.\\-]*) *)", System.Text.RegularExpressions.RegexOptions.Multiline);
			endtagname =  new Regex("^([a-zA-Z][a-zA-Z0-9\\.\\-]*)( *>)", System.Text.RegularExpressions.RegexOptions.Multiline);
			
			commentstart =  new Regex("^(<!)");

			// Missing Pattern.DOTALL
			// comment_working =  new Regex("^(?:--)?(([^>]*?)--)", System.Text.RegularExpressions.RegexOptions.Multiline);
			comment_finish =  new Regex("^([^>]*?)>", System.Text.RegularExpressions.RegexOptions.Multiline);
			
			// Attempts to grab the entire attribute string. This means multiple
			// attributes appear in our string. So we need to parse this substring 
			// again
			attribute =  new Regex(@"^(([a-zA-Z][a-zA-Z0-9.\-_:]*) *)", System.Text.RegularExpressions.RegexOptions.Multiline);
			
			// The value of the attribute. Either in quotes or not
			unquotedvalue =  new Regex("(^= *([^\"\'> ][^> ]*) *)", System.Text.RegularExpressions.RegexOptions.Multiline);
			quotedvalue =  new Regex("(^= *([\"])(.*?)([\"]) *)", System.Text.RegularExpressions.RegexOptions.Multiline);
		}

		/// <summary>
		/// Takes a string of tokenised text and nibbles away at it firing events
		/// as it goes.
		/// </summary>
		/// <param name="text">The tokenised text</param>
		public virtual void Process(System.String text)
		{
			while (true)
			{
				// We do this as we nibble at the text. If the length does not
				// change in a pass then we have stopped nibbling and should 
				// exit.
				int length = text.Length;

				// Match results for our current text with our regular expressions
				System.Text.RegularExpressions.Match text_match = plaintext.Match(text);
				System.Text.RegularExpressions.Match start_match = starttag.Match(text);
				System.Text.RegularExpressions.Match end_match = endtag.Match(text);
				System.Text.RegularExpressions.Match start_comment = commentstart.Match(text);
			
				if (text_match.Success)
				{
					// Consume this part of the text
					text = text.Remove(text_match.Index, text_match.Length);

					// Fire event
					if(this.Text != null)
					{
						TextEvent tagname = new TextEvent(text_match.Value);
						this.Text(this, new TextEventArgs(tagname));
					}
				}
				else if (start_comment.Success)
				{
					// ^(<!)
					// Consume start of comment and eat text till end of comment
					// encountered. Then fire event with comment text
					text = text.Remove(start_comment.Index, start_comment.Length);
					
					// Build a comment body matcher instance.
					// System.Text.RegularExpressions.Match comment_body_matcher = comment_working.Match(text);

					System.Text.StringBuilder comment_text = new System.Text.StringBuilder();
				
					// ^(([^>]*?)--) 
					//	if(comment_body_matcher.matches())
					//	{
					//			text = comment_body_matcher.replaceFirst("");		
					// Assign the comment part to eaten and if there was 
					// text immediately after the <! we treat that as a declaration
					// and store it.
					//			eaten.append(comment_body_matcher.group(1));
					//	}
				
					// Finish the comment tag.

					// Make a new comment end matcher and see if it matches our text
					System.Text.RegularExpressions.Match comment_stop = comment_finish.Match(text);
					
					if (comment_stop.Success)
					{
						// Consume
						text = text.Remove(comment_stop.Index, comment_stop.Length);
						
						// Grab the comment text and fire event
						comment_text.Append(comment_stop.Groups[1].Value.ToString());
					
						// Now this text is either a comment or a declaration. As
						// both start with <! we will have to check.
						string finaltext = comment_text.ToString();

						if(finaltext.StartsWith("--"))
						{
							// Treat as a comment
							if(this.Comment != null)
							{
								CommentEvent tagname = new CommentEvent(finaltext);
								this.Comment(this, new CommentEventArgs(tagname));
							}
						}
						else
						{
							// Treat as a declaration
							if(this.Declaration != null)
							{
								DeclarationEvent tagname = new DeclarationEvent(finaltext);
								this.Declaration(this, new DeclarationEventArgs(tagname));
							}
						}
					}
					else
					{
						// No end point - text event plus an exception.
					}
				}
				else if (end_match.Success)
				{
					//System.Windows.Forms.MessageBox.Show("end tag");
					// Consume and try to see if we have an end tag name or no
					text = text.Remove(end_match.Index, end_match.Length);

					// Make a new matcher for the end tag pattern.
					System.Text.RegularExpressions.Match endtagname_match = endtagname.Match(text);
				
					if (endtagname_match.Success)
					{
						// $self->end(lc($1), "</$1$2");
						text = text.Remove(endtagname_match.Index, endtagname_match.Length);
						
						if(this.EndTag != null)
						{
							EndEvent mytag = new EndEvent(endtagname_match.Groups[1].ToString());
                            this.EndTag(this, new EndTagEventArgs(mytag));
						}
					}
				}
				else if (start_match.Success)
				{
					text = startTagHandler(start_match, text);
				}
				else
				{
					// Nothing recognisable - The end of the file mayhap?
					Console.Out.WriteLine("Could not recognise anything from here onwards");
				}
			
				if (length == text.Length)
				{
					// The string has not changed - a quick hack while we test. 
					// This is definitely the end of the parsing run.
					break;
				}
			}
		}

		/// <summary>
		/// Internal code to handle the start tag. Tries to capture any 
		/// attributes to this tag and checks if the start tag closes. 
		/// 
		/// It also handles the case where the tag starts and ends in one go
		/// (such as the img tag). In these cases two events are fired. Start
		/// and End respectively.
		/// </summary>
		/// <param name="start">The regex match for the start of the tag</param>
		/// <param name="text">The current document text</param>
		private String startTagHandler(System.Text.RegularExpressions.Match start, string text)
		{
			// A start tag with possible attributes
			text = text.Remove(start.Index, start.Length);
						
			// Find the name of the tag.
			System.Text.RegularExpressions.Match tagname_match = tagname.Match(text);
					
			if (tagname_match.Success)
			{
				// Consume the tag
				text = text.Remove(tagname_match.Index, tagname_match.Length);
						
				// Find the attributes within our text
				String eaten = tagname_match.Groups[1].ToString();
				String name = tagname_match.Groups[2].ToString();

				// Grab the attributes of this tag - todo
				System.Text.RegularExpressions.Match attr_match = attribute.Match(text);
				System.Collections.Hashtable attributes = new System.Collections.Hashtable();

				while (attr_match.Success)
				{
					// Replace
					text = text.Remove(attr_match.Index, attr_match.Length);
													
					// Update eaten and build up our attribue stuff
                    String attr = attr_match.Groups[2].ToString().ToLower(System.Globalization.CultureInfo.InvariantCulture);
					eaten = new System.Text.StringBuilder(eaten + attr_match.Groups[2]).ToString();
					String value_Renamed = null;
						
					// Fill in the value and update eaten with what we are looking
					// at
					System.Text.RegularExpressions.Match unquoted_match = unquotedvalue.Match(text);
					System.Text.RegularExpressions.Match quoted_match = quotedvalue.Match(text);
						
					if (unquoted_match.Success)
					{
						// Nibble
						text = text.Remove(unquoted_match.Index, unquoted_match.Length);
						
						eaten = new System.Text.StringBuilder(eaten + unquoted_match.Groups[1]).ToString();
						value_Renamed = unquoted_match.Groups[2].ToString();
					}
					else if (quoted_match.Success)
					{
						// Chew
						text = text.Remove(quoted_match.Index, quoted_match.Length);
						
						eaten = new System.Text.StringBuilder(eaten + quoted_match.Groups[1]).ToString();
						value_Renamed = quoted_match.Groups[3].ToString();
					}
					else
					{
						value_Renamed = new System.Text.StringBuilder(attr).ToString();
					}
						
					// Now store in a hash of attributes
					attributes[attr] = value_Renamed;

					// Go again
					attr_match = attribute.Match(text);
				}

				// Grab the end of the tag. If we don't find it then the tag isn't
				// closed and is not valid.
				System.Text.RegularExpressions.Match close_match = closetag.Match(text);
				System.Text.RegularExpressions.Match closesingle_match = endsingletag.Match(text);
				
				if (close_match.Success)
				{
					// A full tag - the tag was closed
					text = text.Remove(close_match.Index, close_match.Length);
				
					// Send off the tag
					if(this.StartTag != null)
					{
						StartEvent mytag = new StartEvent(name, attributes);
						this.StartTag(this, new StartTagEventArgs(mytag));
					}

					// Double check tags that close but may not have been closed properly.
					// Currently we shall allow: br, hr, meta, img and nobr. This should come
                    // from a config.
					if(
						name.StartsWith("br") || 
						name.StartsWith("hr") ||
						name.StartsWith("meta") ||
						name.StartsWith("nobr") ||
						name.StartsWith("img") ||
						name.StartsWith("input")
					)
					{
						// And now the end
						//System.Windows.Forms.MessageBox.Show(tagname_match.Value);
						//System.Windows.Forms.MessageBox.Show("Close match for '" + tagname_match.Value + "'");
						if(this.EndTag != null)
						{
							EndEvent mytag = new EndEvent(name);
							this.EndTag(this, new EndTagEventArgs(mytag));
						}
					}
				}
				else if (closesingle_match.Success)
				{
					//System.Windows.Forms.MessageBox.Show("Close single match " + closesingle_match.Value + " " + text.Substring(0, 30).ToString());

					// A full tag - the tag was closed
					text = text.Remove(closesingle_match.Index, closesingle_match.Length);
				
					// Send off the start tag
					if(this.StartTag != null)
					{
						//System.Windows.Forms.MessageBox.Show(name);
						StartEvent mytag = new StartEvent(name, attributes);
						this.StartTag(this, new StartTagEventArgs(mytag));
					}

					// And now the end
					if(this.EndTag != null)
					{
						//System.Windows.Forms.MessageBox.Show(name);
						EndEvent mytag = new EndEvent(name);
						this.EndTag(this, new EndTagEventArgs(mytag));
					}
				}
				else
				{
					// Non-compliance :). Treat as text at some point.
					System.Console.Error.WriteLine("Found something: " + text);
				}
			}
			return text;
		} // End method
	
	} // End class

} // End Namespace